package com.hoodbrains.insideloc.locator;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;

/**
 * Singleton dealing with the geolocation thing.
 * 
 * @author archimew
 * 
 */
public class Locator implements RelevantLocationProvider {

	private static Locator instance = null;


	private static final int TWO_MINUTES = 1000 * 60 * 2;
	private static final int THREE_MINUTES = 1000 * 60 * 3;

	
	/** Treshold used to choose the better location. */
	private static final int BAD_ACCURACY_TRESHOLD = 200;
	private static final int BAD_ACCURACY_FACTOR_TRESHOLD = 20;
	private static final long VERY_OLDER_TRESHOLD = TWO_MINUTES;

	/** Obsolete location threshold. */
	private static final long OBSOLETE_LOCATION_TRESHOLD = THREE_MINUTES;
	
	// Location update parameters. Android seems to not well handle
	private static final long MIN_TIME = 0;
	private static final float MIN_DIST = 0;

	private LocationManager manager;
	
	/** Listener of this provider. Should be synchronized.*/
	private Set<RelevantLocationListener> listeners;
	
	/** Implementation  LocationListeners */
	private LocListener gpsListener;
	private LocListener netListener;
	
	private boolean listeningGPS = false;
	private boolean listeningNetwork = false;
	
	private Location currentBestLoc;
	
	private Location lastRelevantLocation;

	/**
	 * Singleton constructor
	 */
	private Locator(Context ctx) {
		manager = (LocationManager) ctx.getSystemService(Context.LOCATION_SERVICE);
		
		listeners = Collections.synchronizedSet(new HashSet<RelevantLocationListener>());

		gpsListener = new LocListener(LocationManager.GPS_PROVIDER);
		netListener = new LocListener(LocationManager.NETWORK_PROVIDER);

		//Try old GPS location.
		currentBestLoc = manager
				.getLastKnownLocation(LocationManager.GPS_PROVIDER);
		
		//Use old network location if it is better
		Location lastNetworkLocation = manager
				.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
		
		if (lastNetworkLocation != null && isBetterLocation(lastNetworkLocation)) {
			currentBestLoc = lastNetworkLocation;
		}
		
		if (currentBestLoc == null) {
			// Dummy initial location, to avoid currentBestLoc == null.
			Location defaultLoc = new Location("mouhaha");
			defaultLoc.setLatitude(37.266403);
			defaultLoc.setLongitude(-115.802765);
			defaultLoc.setAccuracy(BAD_ACCURACY_TRESHOLD * 10);
			defaultLoc.setTime(0);

			currentBestLoc = defaultLoc;
		}
		
		lastRelevantLocation = currentBestLoc;
		
		startLocating(false);
	}
	
	/** Create the singleton. 
	 * Should be called before any call to instance.
	 * 
	 * @param ctx
	 */
	public static void init(Context ctx) {
		instance = new Locator(ctx);
	}
	
	/**
	 * Get instance singleton method
	 * 
	 * @return the instance of the singleton
	 */
	public static Locator getInstance() {
		
		if (instance == null) {
			throw new RuntimeException("Locator: Call getInstance before init() !");
		}

		return instance;
	}

	/**
	 * Private class implementing the LocationListener interface. Tied to a
	 * peculiar location provider type.
	 * 
	 * @author archimew
	 * 
	 */
	private class LocListener implements LocationListener {

		private String provider;

		private LocListener(String provider) {
			this.provider = provider;
		}

		public final String getProvider() {
			return provider;
		}

		public void onLocationChanged(Location location) {
//			Log.e("AndroGrab", "Location changed: " + provider);
			setCurrentBestLoc(location);
		}

		public void onProviderDisabled(String provider) {
			// if (isSameProvider(provider, this.provider))
			// manager.removeUpdates(this);
		}

		public void onProviderEnabled(String provider) {
			// if (isSameProvider(provider, this.provider))
			// manager.requestLocationUpdates(this.provider, TWO_MINUTES,
			// MIN_DIST, this);
		}

		public void onStatusChanged(String provider, int status, Bundle extras) {
			// TODO dunno
		}
	}

	
	public Boolean isAnyLocationEnabled() {
		return (manager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) || manager
				.isProviderEnabled(LocationManager.GPS_PROVIDER));
	}

	public Boolean isGpsLocationEnabled() {
		return manager.isProviderEnabled(LocationManager.GPS_PROVIDER);
	}

	

	/**
	 * Verifies if the new location is the most accurate one and updates
	 * currentBestLoc
	 * 
	 * @param location
	 */
	private synchronized void setCurrentBestLoc(Location location) {
		if (isBetterLocation(location)) {
			currentBestLoc = location;
			
			if (hasLocationChanged()) {
				lastRelevantLocation = currentBestLoc;
				notifyUpdateListeners(true);
			} else {
				notifyUpdateListeners(false);
			}
		}
	}

	/**
	 * Determines whether one Location reading is better than the current
	 * Location fix
	 * 
	 * @param location
	 *            The new Location that you want to evaluate
	 */
	private boolean isBetterLocation(Location location) {
		if (currentBestLoc == null) {
			// A new location is always better than no location
			return true;
		}

		// Check whether the new location fix is newer or older
		long timeDelta = location.getTime() - currentBestLoc.getTime();
		boolean isSignificantlyNewer = timeDelta > VERY_OLDER_TRESHOLD;
		boolean isSignificantlyOlder = timeDelta < -VERY_OLDER_TRESHOLD;
		boolean isNewer = timeDelta > 0;

		// If it's been more than two minutes since the current location, use
		// the new location
		// because the user has likely moved
		if (isSignificantlyNewer) {
			return true;
			// If the new location is more than two minutes older, it must be
			// worse
		} else if (isSignificantlyOlder) {
			return false;
		}

		// Check whether the new location fix is more or less accurate
		int accuracyDelta = (int) (location.getAccuracy() - currentBestLoc
				.getAccuracy());
		boolean isLessAccurate = accuracyDelta > 0;
		boolean isMoreAccurate = accuracyDelta < 0;
		boolean isSignificantlyLessAccurate = accuracyDelta > BAD_ACCURACY_TRESHOLD;

		// Check if the old and new location are from the same provider
		boolean isFromSameProvider = isSameProvider(location.getProvider(),
				currentBestLoc.getProvider());

		// Determine location quality using a combination of timeliness and
		// accuracy
		if (isMoreAccurate) {
			return true;
		} else if (isNewer && !isLessAccurate) {
			return true;
		} else if (isNewer && !isSignificantlyLessAccurate
				&& isFromSameProvider) {
			return true;
		}
		return false;
	}
	
	private boolean hasLocationChanged() {
		//If accuracy became significantly better :
		if (lastRelevantLocation.getAccuracy()/currentBestLoc.getAccuracy() > BAD_ACCURACY_FACTOR_TRESHOLD) {
//		if (currentBestLoc.getAccuracy() - lastRelevantLocation.getAccuracy() < BAD_ACCURACY_TRESHOLD) {
			return true;
		}

		//If we are no more in the previous zone :
		if (lastRelevantLocation.distanceTo(currentBestLoc) > lastRelevantLocation.getAccuracy()) {
			return true;
		}

		return false;
	}
	

	/** Checks whether two providers are the same */
	private boolean isSameProvider(String provider1, String provider2) {
		if (provider1 == null) {
			return provider2 == null;
		}
		return provider1.equals(provider2);
	}
	
	/**
	 * 
	 * @return the location of the user.
	 */
	//@Deprecated or not ?
	public Location getLocation() {
		return currentBestLoc;
	}
	
	/**
	 * 
	 * @return the latitude of the user
	 */
	@Deprecated
	public double getLatitude() {
		return currentBestLoc.getLatitude();
	}

	/**
	 * 
	 * @return the longitude of the user
	 */
	@Deprecated
	public double getLongitude() {
		return currentBestLoc.getLongitude();
	}

	/**
	 * 
	 * @return the altitude of the user
	 */
	@Deprecated
	public double getAltitude() {
		return currentBestLoc.getAltitude();
	}

	/**
	 * 
	 * @return the accuracy of the location.
	 */
	@Deprecated
	public double getAccuracy() {
		return currentBestLoc.getAccuracy();
	}

	@Deprecated
	public double distanceTo(double latitude, double longitude) {
//		Location point = new Location("mouhahah");
//		point.setLatitude(latitude);
//		point.setLongitude(longitude);
//
//		return currentBestLoc.distanceTo(point);
		return distanceBetween(currentBestLoc, latitude, longitude);
	}
	
	public static double distanceBetween(Location loc, double latitude, double longitude) {
		Location point = new Location("mouhahah");
		point.setLatitude(latitude);
		point.setLongitude(longitude);

		return loc.distanceTo(point);
	}
	
	public int getLayer() {
		// TODO implement using accuracy
		return 100;

	}
	
	public static int getLayer(Location loc) {
		//TODO use accuraccy :
		//double accuracy = loc.getAccuracy();
		
		return 100;
	}
			
	
	/**
	 * Starts listening for location updates, if not yet done. 
	 */
	private void startLocating(boolean activateGPS) {
		if (!listeningNetwork) {
			manager.requestLocationUpdates(netListener.getProvider(), 
						MIN_TIME, MIN_DIST, netListener);
		}
		
		if (activateGPS && !listeningGPS) {
			manager.requestLocationUpdates(gpsListener.getProvider(), 
						MIN_TIME, MIN_DIST, gpsListener);
		}
	}

	/**
	 * Stops listening for location updates, if nobody is waiting for a location.
	 */
	private void stopLocating() {
		int nbListeners = 0;
		nbListeners = listeners.size();
		if (nbListeners == 0) {
			manager.removeUpdates(gpsListener);
			manager.removeUpdates(netListener);
		}
	}
	
	// / Location provider methods
	
	private void notifyUpdateListeners(boolean locationChanged) {
		//Avoid synchronization problems.
		//We create a 'view' of the listeners, s listeners synchronized
		//collections can be modified by other threads during the dispatch.
		RelevantLocationListener[] listenersArray = 
			listeners.toArray(new RelevantLocationListener[listeners.size()]);
		
		for (RelevantLocationListener listener : listenersArray) {
			listener.onLocationUpdate(currentBestLoc, locationChanged);
		}
	}
	
	//@Override : if an Interface is created.
	public void subscribeForRelevantUpdates(RelevantLocationListener listener, boolean activateGPS) {
		startLocating(activateGPS);
		listeners.add(listener);
		if ((System.currentTimeMillis() - currentBestLoc.getTime()) < OBSOLETE_LOCATION_TRESHOLD) {
			listener.onLocationUpdate(currentBestLoc, true);
		}
	}
	
	/**
	 * Subscribe for location update. Z : this method can call the listener
	 * synchronously.
	 */
	public void subscribeForRelevantUpdates(RelevantLocationListener listener) {
		subscribeForRelevantUpdates(listener, true);
	}

	public void unSubscribeForRelevantUpdates(RelevantLocationListener listener) {
		listeners.remove(listener);
		stopLocating();
	}

}
